﻿/* ----------------------------------------------------------------------------
 * Copyright (c) Philip Stears 
 *
 * This source code is subject to terms and conditions of the Microsoft Public 
 * License (Ms-PL).
 * 
 * By using this source code in any fashion, you are agreeing to be bound 
 * by the terms of the Microsoft Public License.
 * 
 * To request a copy of the Microsoft Public License, send a request by email
 * to license@connectbasic.net.
 *
 * This notice must not be removed from this software.
 * ---------------------------------------------------------------------------- */

namespace ConnectBasic.Shell.Implementation.DefaultProjectInternals
{
    using System;
    using System.Drawing;
    using System.IO;
    using System.Linq;
    using System.Threading;
    using ConnectBasic.Execution;
    using ConnectBasic.Shell.Extensibility;
    using ConnectBasic.Shell.Hosting;
    using ConnectBasic.Shell.Implementation.DefaultCodeEditor;
    using ConnectDebug;
    using Microsoft.Scripting;
    using Microsoft.Scripting.Hosting;

    internal class StandardModuleItem : IProjectItem, IEditable, ICodeDocument, IMacroExecutionService
    {
        private ScriptEngine mEngine = BasicHelpers.CreateEngine(true);

        private BasicModule mCompiled;
        private FileDebugger mCompiledDebugger;
        private int mCompiledVersion;
        private object mCompiledLock = new object();
        private ManualResetEvent mCompileFinishedEvent = new ManualResetEvent(true);

        private ItemTypeDef mTypeDef;
        private DefaultProject mProject;
        private IProjectStoreItem mStore;
        private string mName;
        private CodeEditor mEditor;

        #region .ctor

        internal StandardModuleItem(ItemTypeDef typeDef, DefaultProject project, IProjectStoreItem store)
        {
            mTypeDef = typeDef;
            mProject = project;
            mStore = store;
            mName = Path.GetFileNameWithoutExtension(store.Name);
        }

        #endregion

        #region IProjectItem Members

        public string Name
        {
            get { return mName; }
        }

        public Image GetSmallImage()
        {
            return mTypeDef.GetSmallImage();
        }

        public string SmallImageKey
        {

            // Use the item type name as the image key because we 
            // use the same image for all instances of standard modules
            get { return WellKnownItemTypeNames.StandardModule; }
        }

        #endregion

        #region IEditable Members

        public bool TryEdit()
        {

            // Try and get hold of the editor service
            var svc = mProject.Shell.GetService<IEditorService>();

            if (svc == null)
            {
                return false;
            }

            // Create the editor if we've not already done so
            if (mEditor == null)
            {
                mEditor = new CodeEditor(this);
                mEditor.BufferChanged += new EventHandler<CodeEditorEventArgs>(mEditor_BufferChanged);
            }

            // Activate it
            return svc.ActivateEditor(mEditor);
        }

        #endregion

        #region ICodeDocument Members

        public string Title
        {
            get { return mName; }
        }

        public string Code
        {
            get
            {
                throw new NotImplementedException();
            }
            set
            {
                throw new NotImplementedException();
            }
        }

        public string FileName
        {
            get { return mStore.Name; }
        }

        #endregion

        #region IMacroExecutionService Members

        public System.Collections.Generic.IEnumerable<IExecutableMacro> GetExecutableMacros()
        {

            // Make sure if there is a compile in-progress that we wait for it
            mCompileFinishedEvent.WaitOne();

            lock (mCompiledLock)
            {
                if (mCompiled != null)
                {
                    return mCompiled.GetFunctions().Where((func) => func.ReturnType == typeof(void) && func.GetParameterCount() == 0).Select((func) => new BasicMacro(func)).ToArray();
                }
            }

            // If we got here, there's no compiled code, just return the functions as-is
            return EmptyArray<IExecutableMacro>.Instance;
        }

        #endregion

        #region Editor Events

        void mEditor_BufferChanged(object sender, CodeEditorEventArgs e)
        {

            // Create an object which can provide services to the editor based
            // on the code snapshot
            var services = new StandardModuleCodeServices(mEngine, e.Snapshot);

            // Attach them
            e.Snapshot.AttachServices(services);

            // Compile the code in the background
            mCompileFinishedEvent.Reset();

            lock (mCompiledLock)
            {
                mCompiled = null;
                mCompiledVersion++;

                ThreadPool.QueueUserWorkItem(new WaitCallback(
                        delegate(object o)
                        {
                            Compile(mCompiledVersion, e.Snapshot);
                        }
                    ));
            }
        }

        #endregion

        #region Private Methods

        private void Compile(int compileVersion, ICodeSnapshot snapshot)
        {
            var source = mEngine.CreateScriptSourceFromString(
                snapshot.GetBufferContents(),
                this.FileName,
                SourceCodeKind.File);

            BasicModule module = null;
            FileDebugger debugger = null;

            // Compile the module and attach a debugger
            try
            {
                module = (BasicModule)source.Execute();

                if (module.IsDebuggable)
                {
                    var ds = mProject.Shell.GetService<IDebuggerService>();

                    if (ds != null && ds.Debugger != null)
                    {

                        // Create a debugger for the individual file
                        debugger = new FileDebugger(module.DebugInfo);

                        // Give that to the shell's debugger so it can
                        // attach it to a debugger which spans all files
                        debugger.Context = ds.Debugger;

                        // Let the module know to call the debugger
                        module.AttachDebugger(debugger);
                    }
                }
            }
            catch (SyntaxErrorException)
            {

                // TODO: Error handling strategy
            }
            catch (Exception)
            {

                // TODO: Error handling strategy
            }

            // Update state
            lock (mCompiledLock)
            {
                if (compileVersion == mCompiledVersion)
                {
                    mCompiled = module;
                    mCompiledDebugger = debugger;
                    mCompileFinishedEvent.Set();
                }
            }
        }

        #endregion

    }
}
